/*
 * Copyright 2007 Agile Plugins Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * 	http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.googlecode.agileplugins.monkey;

import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.DataKeys;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.SearchScope;
import com.intellij.psi.search.searches.ReferencesSearch;
import com.intellij.refactoring.PackageWrapper;
import com.intellij.refactoring.util.RefactoringUtil;
import com.intellij.util.IncorrectOperationException;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

public class CreateAutoboundaryAction extends AnAction {
    private static final String AUTOBOUNDARY_PREFIX = "au.com.suncorp.easydoc.autoboundary.";

    public void actionPerformed(AnActionEvent anActionEvent) {
        DataContext datacontext = anActionEvent.getDataContext();
        final Project project = DataKeys.PROJECT.getData(datacontext);
        if (project == null) {
            System.out.println("no project");
            return;
        }
        PsiDocumentManager.getInstance(project).commitAllDocuments();
        PsiElement element = DataKeys.PSI_ELEMENT.getData(datacontext);
        PsiFile psiFile = DataKeys.PSI_FILE.getData(datacontext);
        if (element instanceof PsiMethod) {
            PsiMethod psiMethod = (PsiMethod) element;
            if (psiMethod.isConstructor()) {
                element = psiMethod.getParent();
            }
        }
        System.out.println("element = " + element);
        System.out.println("psiFile = " + psiFile);

        if (element instanceof PsiClass && psiFile != null) {
            process(project, (PsiClass) element, psiFile);
        }
    }

    private void process(final Project project, final PsiClass psiClass, final PsiFile psiFile) {
        PsiFile containingFile = psiClass.getContainingFile();
        if (containingFile instanceof PsiJavaFile) {
            final PsiJavaFile psiJavaFile = (PsiJavaFile) containingFile;
            final String packageName = psiJavaFile.getPackageName();
            if (packageName.startsWith(AUTOBOUNDARY_PREFIX)) {
                Messages.showErrorDialog("Class " + packageName + "." + psiClass.getName() + " is already in autoboundary!", "AutoBoundary");
                return;
            }
            final Runnable runnable = new Runnable() {

                public void run() {
                    createAutoboundary(packageName, psiFile, psiClass, project);
                }
            };
            CommandProcessor.getInstance().executeCommand(project,
                    new Runnable() {
                        public void run() {
                            ApplicationManager.getApplication().runWriteAction(runnable);
                        }
                    }, "Create Autoboundary", null);
        }
    }

    private void createAutoboundary(String packageName, PsiFile psiFile, PsiClass originalClass, Project project) {
        String newPackage = AUTOBOUNDARY_PREFIX + packageName;
        PsiManager manager = psiFile.getManager();
        final PackageWrapper packageWrapper = new PackageWrapper(manager, newPackage);
        ProjectRootManager rootManager = ProjectRootManager
                .getInstance(project);
        VirtualFile virtualFile = psiFile.getVirtualFile();
        List<PsiMethod> methods = getCalledMethods(originalClass, psiFile);
        List<PsiMethod> staticMethods = new ArrayList<PsiMethod>();
        List<PsiMethod> constructors = new ArrayList<PsiMethod>();
        for (Iterator<PsiMethod> iterator = methods.iterator(); iterator.hasNext();) {
            PsiMethod method = iterator.next();
            boolean constuctor = method.isConstructor();
            boolean staticMethod = method.getModifierList().hasModifierProperty("static");
            if (constuctor) {
                constructors.add(method);
                iterator.remove();
            } else if (staticMethod) {
                staticMethods.add(method);
                iterator.remove();
            }
        }
        PsiClass boundaryClass = null;
        if (!methods.isEmpty()) {
            boundaryClass = getBoundaryClass(project, manager, newPackage, rootManager, virtualFile, packageWrapper, originalClass.getName());
        }
        PsiClass boundaryFactoryClass = null;
        if (constructors.size() + staticMethods.size() > 0) {
            String className = originalClass.getName();
            if (methods.size() + constructors.size() > 0) {
                className += "Factory";
            }
            boundaryFactoryClass = getBoundaryClass(project, manager, newPackage, rootManager, virtualFile, packageWrapper, className);
        }
        String oldClassName = packageName + "." + originalClass.getName();
        PsiClass fieldDestinationBoundary = boundaryClass != null ? boundaryClass : boundaryFactoryClass;
        addFieldsToBoundary(originalClass, fieldDestinationBoundary, manager, psiFile, oldClassName);
        addMethodsToBoundary(methods, boundaryClass);
        addMethodsToBoundary(constructors, boundaryFactoryClass);
        addMethodsToBoundary(staticMethods, boundaryFactoryClass);
        if (boundaryFactoryClass != null) {
            PsiField psiField = addField(psiFile, boundaryFactoryClass.getName());
            replaceStaticCalls(psiFile, psiField, originalClass);
        }
        removeImport(psiFile, oldClassName);
        addBoundaryImport(psiFile, boundaryClass);
        addBoundaryImport(psiFile, boundaryFactoryClass);
        openFileContainingElement(boundaryClass, project);
        openFileContainingElement(boundaryFactoryClass, project);
    }

    private void replaceStaticCalls(PsiFile psiFile, PsiField psiField, PsiClass originalClass) {
        SearchScope searchScope = GlobalSearchScope.fileScope(psiFile);
        PsiManager manager = psiField.getManager();
        Collection<PsiReference> references = ReferencesSearch.search(originalClass, searchScope, true).findAll();
        for (PsiReference reference : references) {
            System.out.println("reference = " + reference);
            if (reference instanceof PsiReferenceExpression) {
                PsiReferenceExpression psiReferenceExpression = (PsiReferenceExpression) reference;
                try {
                    PsiExpression expressionFromText = manager.getElementFactory().createExpressionFromText(psiField.getName(), psiField);
                    psiReferenceExpression.replace(expressionFromText);
                } catch (IncorrectOperationException e) {
                    throw new RuntimeException(e);
                }
            } else if (reference instanceof PsiJavaCodeReferenceElement) {
                PsiJavaCodeReferenceElement psiJavaCodeReferenceElement = (PsiJavaCodeReferenceElement) reference;
                PsiElement parent = psiJavaCodeReferenceElement.getParent();
                if (parent instanceof PsiNewExpression) {
                    PsiNewExpression psiNewExpression = (PsiNewExpression) parent;
                    try {
                        PsiExpression expressionFromText = manager.getElementFactory().createExpressionFromText(psiField.getName() + ".new" + psiNewExpression.getText().substring(4), psiNewExpression);
                        psiNewExpression.replace(expressionFromText);
                    } catch (IncorrectOperationException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    }

    private PsiField addField(PsiFile psiFile, String originalClassName) {
        PsiJavaFile psiJavaFile = (PsiJavaFile) psiFile;
        PsiClass psiClass = psiJavaFile.getClasses()[0];
        PsiField[] fields = psiClass.getFields();
        for (int i = 0; i < fields.length; i++) {
            PsiField field = fields[i];
            if (field.getType().getPresentableText().equals(originalClassName)) {
                return field;
            }
        }
        String fieldName = StringUtil.lcFirst(originalClassName);
        String field = originalClassName + " " + fieldName + ";";
        try {
            PsiField fieldFromText = psiClass.getManager().getElementFactory().createFieldFromText(field, psiClass);
            if (fields.length == 0) {
                return (PsiField) psiClass.addAfter(fieldFromText, psiClass.getLBrace());
            } else {
                return (PsiField) psiClass.addAfter(fieldFromText, fields[fields.length - 1]);
            }
        } catch (IncorrectOperationException e) {
            throw new RuntimeException(e);
        }
    }

    private void addMethodsToBoundary(List<PsiMethod> methods, PsiClass boundaryClass) {
        for (Iterator<PsiMethod> iterator = methods.iterator(); iterator.hasNext();) {
            PsiMethod psiMethod = iterator.next();
            addMethod(psiMethod, boundaryClass);
        }
    }

    private PsiClass getBoundaryClass(Project project, PsiManager manager, String newPackage, ProjectRootManager rootManager, VirtualFile virtualFile, PackageWrapper packageWrapper, String className) {
        PsiClass boundaryClass = lookupClass(project, manager, newPackage, className);
        if (boundaryClass == null) {
            boundaryClass = createClass(rootManager, virtualFile, packageWrapper, className);
        }
        return boundaryClass;
    }

    private List<PsiMethod> getCalledMethods(PsiClass originalClass, PsiFile psiFile) {
        List<PsiMethod> list = new ArrayList<PsiMethod>();
        PsiMethod[] allMethods = originalClass.getAllMethods();
        for (PsiMethod method : allMethods) {
            Collection<PsiReference> references = ReferencesSearch.search(method, GlobalSearchScope.fileScope(psiFile), false).findAll();
            if (references.size() > 0) {
                list.add(method);
            }
        }
        return list;
    }

    private void addFieldsToBoundary(PsiClass originalClass, PsiClass boundaryClass, PsiManager manager, PsiFile psiFile, String originalClassName) {
        PsiField[] allFields = originalClass.getAllFields();
        for (PsiField field : allFields) {
            Collection<PsiReference> references = ReferencesSearch.search(field, GlobalSearchScope.fileScope(psiFile), false).findAll();
            if (references.size() > 0) {
                try {
                    PsiField fieldFromText = manager.getElementFactory().createFieldFromText(field.getType().getPresentableText() + " " + field.getName() + " = " + originalClassName + "." + field.getName() + ";", boundaryClass);
                    PsiField[] fields = boundaryClass.getFields();
                    if (!checkForMatchingField(fields, field)) {
                        if (fields.length == 0) {
                            boundaryClass.addAfter(fieldFromText, boundaryClass.getLBrace());
                        } else {
                            boundaryClass.addAfter(fieldFromText, fields[fields.length - 1]);
                        }
                    }
                } catch (IncorrectOperationException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    private boolean checkForMatchingField(PsiField[] fields, PsiField field) {
        for (int j = 0; j < fields.length; j++) {
            PsiField psiField = fields[j];
            if (psiField.getName().equals(field.getName())) {
                return true;
            }
        }
        return false;
    }

    private void addMethod(PsiMethod method, PsiClass boundaryClass) {
        String newMethod;
        if (method.isConstructor()) {
            newMethod = method.getName() + " new" + method.getName();
        } else {
            newMethod = method.getReturnType().getPresentableText() + " " + method.getName();
        }
        String methodText = newMethod + method.getParameterList().getText() + ";";
        try {
            PsiMethod methodFromText = boundaryClass.getManager().getElementFactory().createMethodFromText(methodText, boundaryClass);
            addIfNotExists(boundaryClass, methodFromText);
        } catch (IncorrectOperationException e) {
            throw new RuntimeException(e);
        }
    }

    private void addIfNotExists(PsiClass boundaryClass, PsiMethod methodFromText) throws IncorrectOperationException {
        PsiMethod methodBySignature = boundaryClass.findMethodBySignature(methodFromText, true);
        if (methodBySignature == null) {
            boundaryClass.add(methodFromText);
        } else {
            System.out.println("methodBySignature = " + methodBySignature);
        }
    }

    private void removeImport(PsiFile psiFile, String oldClassName) {
        if (psiFile instanceof PsiJavaFile) {
            PsiJavaFile psiJavaFile = (PsiJavaFile) psiFile;
            PsiImportList importList = psiJavaFile.getImportList();
            if (importList == null) {
                return;
            }
            PsiImportStatement singleClassImportStatement = importList.findSingleClassImportStatement(oldClassName);
            if (singleClassImportStatement != null) {
                try {
                    singleClassImportStatement.delete();
                } catch (IncorrectOperationException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    private void addBoundaryImport(PsiFile psiFile, PsiClass boundaryClass) {
        if (boundaryClass == null) {
            return;
        }
        PsiElementFactory elementFactory = psiFile.getManager().getElementFactory();
        try {
            PsiImportStatement importStatement = elementFactory.createImportStatement(boundaryClass);
            PsiJavaFile psiJavaFile = (PsiJavaFile) psiFile;
            PsiImportList importList = psiJavaFile.getImportList();
            PsiImportStatement singleClassImportStatement = importList.findSingleClassImportStatement(importStatement.getQualifiedName());
            if (singleClassImportStatement == null) {
                importList.add(importStatement);
            }
        } catch (IncorrectOperationException e) {
            throw new RuntimeException(e);
        }
    }

    private PsiClass createClass(ProjectRootManager rootManager, VirtualFile virtualFile, PackageWrapper packageWrapper, String className) {
        try {
            ProjectFileIndex fileIndex = rootManager.getFileIndex();
            final VirtualFile sourceRootForFile = fileIndex.getSourceRootForFile(virtualFile);
            PsiDirectory packageDirectoryInSourceRoot = RefactoringUtil.createPackageDirectoryInSourceRoot(packageWrapper, sourceRootForFile);
            return packageDirectoryInSourceRoot.createInterface(className);
        } catch (IncorrectOperationException e) {
            throw new RuntimeException(e);
        }
    }

    private PsiClass lookupClass(Project project, PsiManager manager, String boundaryPackage, String newClassName) {
        GlobalSearchScope globalsearchscope = GlobalSearchScope.allScope(project);
        return manager.findClass(boundaryPackage + "." + newClassName, globalsearchscope);
    }

    private void openFileContainingElement(PsiElement createdElement, Project project) {
        if (createdElement == null) {
            return;
        }
        VirtualFile virtualFile = createdElement.getContainingFile().getVirtualFile();
        FileEditorManager fileEditorManager = FileEditorManager.getInstance(project);
        fileEditorManager.openFile(virtualFile, true);
    }
}
